﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Fadd.Components
{
	/// <summary>
	/// Used to scan after components in assemblies.
	/// </summary>
	/// <example>
	/// <code>
	/// // Find all components that exist in different dlls.
	/// ComponentFinder finder = new ComponentFinder();
	/// finder.Find("C:\\myapp\\plugin*.dll");
	/// 
	/// // and add them to our component manager.
	/// ComponentManager manager = new ComponentManager();
	/// manager.Add(finder.Components);
	/// </code>
	/// </example>
	[Serializable]
	public class ComponentFinder
	{
		private List<Component> _components = new List<Component>();
		private FileSystemWatcher _watcher;

		/// <summary>
		/// All found components.
		/// </summary>
		public IEnumerable<Component> Components
		{
			get { return _components; }
		}

		/// <summary>
		/// Add all components found in the specified assemblies.
		/// </summary>
		/// <param name="assemblies">A list of assemblies to search for components in.</param>
		public void Find(List<Assembly> assemblies)
		{
			foreach (Assembly assembly in assemblies)
				AddComponentsFromAssembly(assembly);
		}

		/// <summary>
		/// Watch a directory for new DLLs.
		/// </summary>
		/// <param name="path">Directory to watch</param>
		/// <param name="filter">File mask in directory to watch</param>
		/// <exception cref="InvalidOperationException">Only one directory can be watched per ComponentFinder instance.</exception>
		public void WatchDirectory(string path, string filter)
		{
			if (_watcher != null)
				throw new InvalidOperationException("Only one directory can be watched per ComponentFinder instance.");

			_watcher = new FileSystemWatcher(path, filter);
			_watcher.Created += OnNewFile;
		}

		private void OnNewFile(object sender, FileSystemEventArgs e)
		{
			List<Component> components = ScanForFiles(e.FullPath);
			foreach (var component in components)
				ComponentFound(this, new ComponentFoundEventArgs(component));
		}

		private void AddComponentsFromAssembly(Assembly assembly)
		{
			foreach (Type type in assembly.GetTypes())
			{
				foreach (object attribute in type.GetCustomAttributes(true))
				{
					if (!(attribute is ComponentAttribute))
						continue;

					ComponentAttribute componentAttribute = (ComponentAttribute)attribute;
					_components.Add(new Component
					                	{
					                		InstanceType = type,
					                		InterfaceType = componentAttribute.InterfaceType,
					                		IsNotSingleton = (componentAttribute.Flags & ComponentFlags.NotSingleton) != 0,
											IsInternal = (componentAttribute.Flags & ComponentFlags.Internal) != 0,
					                		RunAt = componentAttribute.RunAt,
					                		Version = componentAttribute.Version
					                	});
				}
			}
		}

		/// <summary>
		/// Find components in the specified path.
		/// </summary>
		/// <param name="fullPath">Full path or just a directory where the DLLs are located.</param>
		/// <exception cref="ReflectionTypeLoadException"></exception>
		/// <exception cref="TypeLoadException"></exception>
		/// <example>
		/// <code>
		/// // Find all components that exist in different dlls.
		/// ComponentFinder finder = new ComponentFinder();
		/// finder.Find("C:\\myapp\\plugin*.dll");
		/// 
		/// // and add them to our component manager.
		/// ComponentManager manager = new ComponentManager();
		/// manager.Add(finder.Components);
		/// </code>
		/// </example>
		public void Find(string fullPath)
		{
			List<Component> components = ScanForFiles(fullPath);
			_components = components;
		}

		/// <summary>
		/// Find components in the specified path.
		/// </summary>
		/// <param name="fullPath">Full path or just a directory where the DLLs are located.</param>
		/// <exception cref="ReflectionTypeLoadException"></exception>
		/// <exception cref="TypeLoadException"></exception>
		/// <example>
		/// <code>
		/// // Find all components that exist in different dlls.
		/// ComponentFinder finder = new ComponentFinder();
		/// finder.Find("C:\\myapp\\plugin*.dll");
		/// 
		/// // and add them to our component manager.
		/// ComponentManager manager = new ComponentManager();
		/// manager.Add(finder.Components);
		/// </code>
		/// </example>
		private List<Component> ScanForFiles(string fullPath)
		{
			AppDomain domain = AppDomain.CreateDomain("ModuleLoader");

			ComponentFinder finder = (ComponentFinder)domain.CreateInstanceFromAndUnwrap(
													GetType().Assembly.Location ?? string.Empty, GetType().Namespace + ".ComponentFinder");

			finder.FindInternal(fullPath);
			List<Component> components = new List<Component>();
			foreach (var component in finder._components)
				components.Add(component);
			AppDomain.Unload(domain);
			return components;
		}

		/// <summary>
		/// Should be run from a separate application domain, to avoid all scanned assemblies being loaded 
		/// into into the current one.
		/// </summary>
		/// <param name="fullPath">Path and wild cards.</param>
		/// <exception cref="TypeLoadException"></exception>
		protected void FindInternal(string fullPath)
		{
			string searchString;
			string directory;
			if (fullPath.Contains("."))
			{
				int pos = fullPath.LastIndexOf('\\');
				if (pos == -1)
				{
					searchString = fullPath;
					directory = Environment.CurrentDirectory;
				}
				else
				{
					searchString = fullPath.Substring(pos + 1);
					directory = fullPath.Substring(0, pos);
				}
			}
			else
			{
				searchString = "*.dll";
				directory = fullPath;
			}

			foreach (string fileName in Directory.GetFiles(directory, searchString))
			{
				try
				{
					Assembly assembly;
					try
					{
						assembly = Assembly.LoadFrom(fileName);
					}
					catch (FileLoadException err)
					{
						Console.WriteLine(err);
						continue;
					}

					AddComponentsFromAssembly(assembly);
				}
				catch (ReflectionTypeLoadException e)
				{
					throw new TypeLoadException("Couldn't load component: " + fileName, e);
				}
			}
		}

		/// <summary>
		/// A new component have been discovered by the file system watcher.
		/// </summary>
		public event EventHandler<ComponentFoundEventArgs> ComponentFound = delegate { };
	}
}
